defer & async

JavaScript Defer vs Async Explained (2025-04-09 17:24:22)

Question:使用 defer 或者 async 异步下载,是新开了一个线程吗
Answer:不是的,defer 和 async 并不会开启新的线程。JavaScript 是单线程的语言,这两个属性只是改变了脚本的加载和执行时序。让我来详细解释一下:
  1. defer 属性:
  • 脚本的下载是异步的,不会阻塞 HTML 解析
  • 脚本会等到 HTML 解析完成后,按照它们在文档中的顺序执行
  • 在 DOMContentLoaded 事件之前执行完成
1
<script defer src="script.js"></script>
  1. async 属性:
  • 脚本的下载是异步的,不会阻塞 HTML 解析
  • 脚本下载完成后立即执行,不保证执行顺序
  • 可能在 DOMContentLoaded 事件之前或之后执行
1
<script async src="script.js"></script>
它们的主要区别可以用图表示:普通脚本:
1
2
HTML 解析 ----||---- HTML 解析
脚本下载和执行
defer:
1
2
3
HTML 解析 -------------------|
脚本下载 ------| |
脚本执行 ----|
async:
1
2
3
HTML 解析 ------------|
脚本下载 ------| |
脚本执行 --|
这些都是在主线程上进行的,只是通过不同的调度方式来优化加载性能。如果真的需要多线程处理,需要使用 Web Workers。
Question:浏览器本身可以并发请求,如果在一个页面底部加载 script 时,加不加 defer 有区别吗
Answer:如果脚本标签放在页面底部(之前),加不加 defer 实际上区别很小,因为此时 HTML 已经基本解析完成。不过还是存在一些细微差别:
  1. 不加 defer 的情况:
    1
    2
    3
    4
    5
    <body>
    <!-- HTML 内容 -->
    <script src="script1.js"></script>
    <script src="script2.js"></script>
    </body>
  • 脚本会按顺序下载和执行
  • script1.js 必须下载并执行完成后,才会开始处理 script2.js
  • 会阻塞 DOMContentLoaded 事件
  1. 加上 defer 的情况:
    1
    2
    3
    4
    5
    <body>
    <!-- HTML 内容 -->
    <script defer src="script1.js"></script>
    <script defer src="script2.js"></script>
    </body>
  • 脚本会并行下载
  • 仍然按照顺序执行,但是不会互相阻塞下载
  • 会在 DOMContentLoaded 事件之前执行
主要区别是:
  1. 并行下载:defer 允许多个脚本并行下载,而普通脚本是串行的
  2. DOMContentLoaded 时机:defer 脚本会在 DOMContentLoaded 之前执行完成,而普通脚本会阻塞这个事件
所以即使在页面底部,使用 defer 仍然可能带来一些性能优势,特别是当有多个脚本文件需要加载的时候。但如果只有一个脚本文件,差异就不明显了。建议:
  • 如果脚本之间有依赖关系,使用 defer 更安全,因为它保证执行顺序
  • 如果是独立的脚本,可以考虑使用 async,执行时机更灵活
  • 如果只有一个简单的脚本,放在底部不加 defer 也是可以的

Question:既然不加defer 的时候,脚本按顺序下载执行,那么浏览器的并发请求还有什么用呢
Answer:浏览器的并发请求主要用于其他资源的并行下载,而不仅仅是脚本文件。让我解释一下:
  1. 浏览器并发请求的应用场景:
  • 图片资源 (img)
  • 样式文件 (css)
  • 字体文件 (font)
  • 媒体文件 (video/audio)
  • XHR/Fetch 请求
  • 其他静态资源
例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
<head>
<link rel="stylesheet" href="style1.css">
<link rel="stylesheet" href="style2.css">
</head>
<body>
<img src="image1.jpg">
<img src="image2.jpg">
<img src="image3.jpg">

<!-- 这些脚本会串行执行 -->
<script src="script1.js"></script>
<script src="script2.js"></script>
</body>
在上面的例子中:
  • 所有图片会并行下载
  • 两个 CSS 文件会并行下载
  • 但是脚本文件会串行下载和执行
  1. 为什么脚本要串行?
  • JavaScript 可能会修改 DOM
  • 脚本之间可能有依赖关系
  • 保证执行顺序的可预测性
  • 避免竞态条件
  1. 现代优化方案:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <!-- 并行下载,按序执行 -->
    <script defer src="script1.js"></script>
    <script defer src="script2.js"></script>

    <!-- 并行下载,谁先下载完谁先执行 -->
    <script async src="independent1.js"></script>
    <script async src="independent2.js"></script>

    <!-- 预加载提示 -->
    <link rel="preload" href="script1.js" as="script">
所以浏览器的并发请求机制主要是为了:
  1. 优化整体页面加载性能
  2. 充分利用带宽资源
  3. 减少用户等待时间
  4. 提高资源加载效率
而脚本的串行执行是出于程序逻辑的正确性考虑,这是两个不同层面的问题。
Question:确认下js文件下载和执行都是串行的吗
Answer:我需要纠正之前的说法。对于普通的 script 标签(不带 defer/async):
  1. 下载过程
  • 下载是可以并行的
  • 浏览器通常会限制并发连接数(通常是每个域名 6-8 个并发)
  • 多个 script 标签的资源可以同时下载
  1. 执行过程
  • 执行是严格串行的
  • 必须按照 script 标签在文档中的顺序执行
  • 一个脚本执行完成后才会执行下一个
例如:
1
2
<script src="big-file.js"></script>  <!-- 5MB, 需要10秒下载 -->
<script src="small-file.js"></script> <!-- 1KB, 需要1秒下载 -->
实际过程是:
  1. big-file.js 和 small-file.js 会同时开始下载
  2. small-file.js 可能先下载完成,但要等 big-file.js 下载并执行完才能执行
  3. 执行顺序一定是 big-file.js 然后是 small-file.js
我为之前说"下载是串行的"这个错误表述道歉。正确的说法是:
  • 下载可以并行
  • 执行必须串行
  • 后面的脚本执行必须等待前面的脚本下载并执行完成
这也是为什么 defer 和 async 这样的属性很有用:
  • defer:并行下载,按序执行
  • async:并行下载,下载完就执行,不保证顺序

Question:如果是并行下载,串行执行的,那加不加 defer 还有什么区别
Answer:主要区别在于 defer 改变了脚本的执行时机和 HTML 解析的阻塞行为:
  1. 不带 defer 的普通脚本
    1
    2
    <script src="script1.js"></script>
    <script src="script2.js"></script>
  • 并行下载脚本
  • 遇到 script 标签时会暂停 HTML 解析
  • 等待当前脚本下载并执行完成后,才继续解析 HTML
  • 执行时机:立即执行(阻塞 HTML 解析)
过程示意:
1
2
HTML解析 ----||---- HTML解析 ----||---- HTML解析
script1下载 script1执行 script2下载 script2执行
  1. 带 defer 的脚本
    1
    2
    <script defer src="script1.js"></script>
    <script defer src="script2.js"></script>
  • 并行下载脚本
  • 不阻塞 HTML 解析,HTML 可以继续解析
  • 所有脚本等到 HTML 解析完成后才执行
  • 执行时机:HTML 解析完成后,DOMContentLoaded 事件之前
过程示意:
1
2
3
4
5
6
HTML解析 --------------------------|
script1下载 -------| |
script2下载 ----------| |
script1执行--|
script2执行--|
DOMContentLoaded 事件 ------------>
主要优势:
  1. 更好的性能
    • HTML 解析不被阻塞
    • 用户可以更快看到页面内容
    • 特别适合放在 <head> 中的脚本
  2. 更可预测的执行环境
    • 确保在 DOM 完全准备好后执行
    • 不用担心 DOM 元素是否存在
    • 适合依赖 DOM 的脚本
所以即使下载都是并行的,defer 通过改变执行时机,仍然带来了明显的优势。特别是在:
  • 脚本放在 <head> 中时
  • 脚本依赖 DOM 结构时
  • 页面加载性能关键时

Question:多个加了 defer 的 script 执行顺序是什么样
Answer:多个带 defer 的脚本特点是:
  • 并行下载
  • 按照在 HTML 中出现的顺序执行
  • 都在 HTML 解析完成后,DOMContentLoaded 之前执行
例如:
1
2
3
<script defer src="script1.js"></script> <!-- 5MB,需要10秒下载 -->
<script defer src="script2.js"></script> <!-- 1KB,需要1秒下载 -->
<script defer src="script3.js"></script> <!-- 2KB,需要2秒下载 -->
执行过程:
  1. script1.js、script2.js、script3.js 同时开始下载
  2. 虽然 script2.js 可能最先下载完成,但要等 script1.js 执行完才能执行
  3. script3.js 必须等 script2.js 执行完才能执行
时序示意图:
1
2
3
4
5
6
7
8
HTML解析 --------------------------------|
script1下载(10s) ----------| |
script2下载(1s) -| |
script3下载(2s) --| |
script1执行 -------|
script2执行 -------|
script3执行 -------|
DOMContentLoaded 事件 ----------------->
这种行为的好处是:
  1. 保证了脚本的执行顺序,适合有依赖关系的脚本
    1
    2
    <script defer src="jquery.js"></script>
    <script defer src="jquery-plugin.js"></script> <!-- 依赖 jquery -->
  2. 比较 async 的行为:
    1
    2
    <script async src="script1.js"></script>
    <script async src="script2.js"></script>
  • async 也是并行下载
  • 但是谁先下载完谁先执行
  • 不保证执行顺序
  • 适合独立的脚本(如统计代码、广告代码等)
所以选择建议:
  • 有依赖关系的脚本用 defer
  • 完全独立的脚本用 async
  • 都不加则会阻塞 HTML 解析

Question:css 可以添加 prefetch 吗
Answer:是的,CSS 文件可以使用 prefetch,而且还有其他几种预加载的方式。我来详细解释一下:
  1. prefetch(预获取):
    1
    <link rel="prefetch" href="styles.css" as="style">
  • 低优先级加载
  • 浏览器空闲时获取
  • 为未来可能用到的资源做准备
  • 适合下一个页面可能需要的资源
  1. preload(预加载):
    1
    <link rel="preload" href="critical.css" as="style">
  • 高优先级加载
  • 当前页面必需的资源
  • 尽早发起请求
  • 适合当前页面关键资源
  1. preconnect(预连接):
    1
    <link rel="preconnect" href="https://cdn.example.com">
  • 预先建立连接
  • 包括 DNS 查找、TCP 握手、TLS 协商
  • 适合确定要从某个域名加载资源,但不确定具体是什么资源
  1. dns-prefetch(DNS预解析):
    1
    <link rel="dns-prefetch" href="https://cdn.example.com">
  • 仅预解析 DNS
  • 比 preconnect 轻量
  • 适合可能会用到的域名
使用建议:
  1. 当前页面关键 CSS:
    1
    2
    <link rel="preload" href="critical.css" as="style">
    <link rel="stylesheet" href="critical.css">
  2. 下一个页面可能需要的 CSS:
    1
    <link rel="prefetch" href="next-page.css" as="style">
  3. 从 CDN 加载的 CSS:
    1
    2
    <link rel="preconnect" href="https://cdn.example.com">
    <link rel="stylesheet" href="https://cdn.example.com/styles.css">
注意事项:
  1. preload 的资源如果 3s 内没有被使用,浏览器控制台会警告
  2. prefetch 的资源可能会被浏览器忽略(如移动设备或弱网环境)
  3. 不要过度预加载,会占用带宽
  4. preload 和实际的资源引用都要写,不然资源不会被使用
选择建议:
  • 当前页面必需的 CSS → preload
  • 下一个页面可能需要的 CSS → prefetch
  • 跨域资源 → preconnect
  • 只需要 DNS 优化 → dns-prefetch

Question:webpack 中配置 externals 的,还会被打包吗
Answer:不会,配置为 externals 的模块会被排除在打包文件之外。这些模块被认为是外部依赖,webpack 不会对其进行打包处理。举个例子:
  1. webpack 配置:
    1
    2
    3
    4
    5
    6
    7
    8
    // webpack.config.js
    module.exports = {
    externals: {
    jquery: 'jQuery', // 全局变量名
    react: 'React',
    'react-dom': 'ReactDOM'
    }
    }
  2. 在代码中使用:
    1
    2
    3
    4
    5
    6
    import $ from 'jquery';     // 不会被打包
    import React from 'react'; // 不会被打包

    // 使用这些库
    $('#app');
    React.createElement('div');
  3. HTML 中需要手动引入:
    1
    2
    3
    <!-- 需要在 HTML 中手动引入这些外部依赖 -->
    <script src="https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/react@17.0.2/umd/react.production.min.js"></script>
externals 的常见使用场景:
  1. 使用 CDN 资源
    • 减小打包体积
    • 利用 CDN 缓存
    • 并行加载提升性能
  2. 避免重复打包
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // webpack.config.js
    module.exports = {
    externals: {
    // 对于 commonjs 模块
    lodash: 'lodash',

    // 对于 UMD 模块
    jquery: {
    commonjs: 'jquery',
    commonjs2: 'jquery',
    amd: 'jquery',
    root: '$' // 全局变量名
    }
    }
    }
  3. 不同格式的 externals:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    module.exports = {
    externals: {
    // 简单的全局变量
    jquery: 'jQuery',

    // 对象形式,指定不同模块系统
    react: {
    root: 'React',
    commonjs2: 'react',
    commonjs: 'react',
    amd: 'react'
    },

    // 数组形式
    subtract: ['./math', 'subtract'],

    // 正则形式
    /^(jquery|\$)$/i: 'jquery'
    }
    }
注意事项:
  1. externals 配置的模块必须在运行环境中可用
  2. 需要确保 HTML 中引入的版本与代码兼容
  3. 注意全局变量名的大小写
  4. 考虑模块加载顺序(如果模块间有依赖关系)

Question:网络浏览器面板中的优化级:high,highest, low, lowest ,如何在前端控制这些资源的优先级呢
Answer:前端可以通过多种方式控制资源的加载优先级:
  1. 使用 fetchpriority 属性(最新的显式优先级控制):
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <!-- 图片优先级 -->
    <img src="hero.jpg" fetchpriority="high" />
    <img src="lazy.jpg" fetchpriority="low" />

    <!-- 脚本优先级 -->
    <script src="critical.js" fetchpriority="high"></script>
    <script src="non-critical.js" fetchpriority="low"></script>

    <!-- CSS优先级 -->
    <link rel="stylesheet" href="critical.css" fetchpriority="high" />
  2. 使用 preload/prefetch 影响优先级:
    1
    2
    3
    4
    5
    6
    <!-- 高优先级加载 -->
    <link rel="preload" href="critical.css" as="style">
    <link rel="preload" href="hero.jpg" as="image">

    <!-- 低优先级加载 -->
    <link rel="prefetch" href="next-page.css" as="style">
  3. 通过资源位置影响优先级:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    <head>
    <!-- head中的CSS默认highest优先级 -->
    <link rel="stylesheet" href="critical.css">

    <!-- head中的script默认为low -->
    <script src="script.js"></script>

    <!-- 添加async降低优先级 -->
    <script async src="async-script.js"></script>
    </head>
    <body>
    <!-- 视口中的图片默认high优先级 -->
    <img src="hero.jpg" />

    <!-- 延迟加载的图片low优先级 -->
    <img loading="lazy" src="lazy.jpg" />
    </body>
  4. 使用 JavaScript 动态控制:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // 使用 Fetch API 设置优先级
    fetch('/api/data', {
    priority: 'high' // 'high' | 'low' | 'auto'
    });

    // 动态创建元素时设置优先级
    const img = new Image();
    img.fetchPriority = 'high';
    img.src = 'hero.jpg';

    // 使用 Priority Hints API
    const hint = document.createElement('link');
    hint.rel = 'preload';
    hint.as = 'image';
    hint.href = 'hero.jpg';
    hint.fetchPriority = 'high';
    document.head.appendChild(hint);
  5. 常见资源的默认优先级:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <!-- Highest -->
    <link rel="stylesheet"> <!-- 在 head 中的 CSS -->

    <!-- High -->
    <img src="hero.jpg"> <!-- 视口内的图片 -->
    <script src="sync.js"> <!-- body 底部的同步脚本 -->

    <!-- Low -->
    <img loading="lazy" src="lazy.jpg"> <!-- 懒加载图片 -->
    <script async src="async.js"> <!-- 异步脚本 -->

    <!-- Lowest -->
    <link rel="prefetch"> <!-- 预获取资源 -->
最佳实践:
  1. 关键资源优先级提升:
    1
    2
    3
    <!-- 首屏关键资源 -->
    <link rel="stylesheet" href="critical.css" fetchpriority="high">
    <img src="hero.jpg" fetchpriority="high">
  2. 非关键资源优先级降低:
    1
    2
    3
    <!-- 非关键资源 -->
    <img loading="lazy" fetchpriority="low" src="below-fold.jpg">
    <script defer fetchpriority="low" src="non-critical.js"></script>
  3. 组合使用不同策略:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    <!-- 预连接 + 高优先级 -->
    <link rel="preconnect" href="https://cdn.example.com">
    <link rel="stylesheet"
    href="https://cdn.example.com/style.css"
    fetchpriority="high">

    <!-- 预加载 + 异步加载 -->
    <link rel="preload" href="font.woff2" as="font" crossorigin>
    <script async src="analytics.js" fetchpriority="low"></script>
注意事项:
  1. fetchpriority 是较新的属性,需要注意浏览器兼容性
  2. 不要过度使用高优先级,会影响整体性能
  3. 优先级设置要结合实际场景和用户体验
  4. 可以通过 Chrome DevTools 的 Network 面板观察优先级效果

Question:rel=preload 添加到 css 资源上的作用
Answer:rel="preload" 添加到 CSS 资源上主要用于提前声明重要的 CSS 资源,让浏览器更早地发现并下载这些资源。主要作用和使用场景如下:
  1. 基本用法
    1
    2
    3
    4
    <!-- 预加载关键 CSS -->
    <link rel="preload" href="critical.css" as="style">
    <!-- 实际使用该 CSS -->
    <link rel="stylesheet" href="critical.css">
  2. 主要作用
  • 提高优先级(设置为 Highest)
  • 提前发现资源
  • 并行下载
  • 不阻塞 HTML 解析
  1. 常见使用场景
a. 关键 CSS 预加载
1
2
3
4
5
6
7
8
9
10
11
12
<head>
<!-- 预加载首屏关键样式 -->
<link rel="preload" href="critical.css" as="style">
<link rel="preload" href="header.css" as="style">

<!-- 实际引用 -->
<link rel="stylesheet" href="critical.css">
<link rel="stylesheet" href="header.css">

<!-- 非关键CSS可以用 prefetch -->
<link rel="prefetch" href="footer.css" as="style">
</head>
b. 字体文件预加载
1
2
3
4
5
6
7
<link 
rel="preload"
href="font.woff2"
as="font"
type="font/woff2"
crossorigin
>
c. 条件加载
1
2
3
4
5
6
7
<!-- 响应式设计中的条件预加载 -->
<link
rel="preload"
href="desktop.css"
as="style"
media="(min-width: 768px)"
>
  1. 注意事项
a. 必须配对使用
1
2
3
4
5
6
<!-- 错误:预加载但未使用 -->
<link rel="preload" href="style.css" as="style">

<!-- 正确:预加载并使用 -->
<link rel="preload" href="style.css" as="style">
<link rel="stylesheet" href="style.css">
b. 避免重复加载
1
2
3
4
<!-- 不好的实践:重复的 URL -->
<link rel="preload" href="style.css" as="style">
<link rel="preload" href="style.css" as="style"> <!-- 重复! -->
<link rel="stylesheet" href="style.css">
c. 正确设置属性
1
2
3
4
5
6
7
8
<!-- 完整的预加载声明 -->
<link
rel="preload"
href="style.css"
as="style"
type="text/css"
crossorigin="anonymous"
>
  1. 性能优化建议
a. 组合使用预加载策略
1
2
3
4
5
6
7
8
9
10
11
<!-- DNS预解析 -->
<link rel="dns-prefetch" href="https://cdn.example.com">

<!-- 预连接 -->
<link rel="preconnect" href="https://cdn.example.com">

<!-- 预加载关键资源 -->
<link rel="preload" href="https://cdn.example.com/critical.css" as="style">

<!-- 预获取未来可能需要的资源 -->
<link rel="prefetch" href="https://cdn.example.com/non-critical.css" as="style">
b. 优先级控制
1
2
3
4
5
6
7
<!-- 最高优先级加载 -->
<link
rel="preload"
href="critical.css"
as="style"
fetchpriority="high"
>
使用建议:
  1. 只预加载当前页面必需的关键 CSS
  2. 确保预加载的资源会被使用
  3. 注意浏览器的缓存策略
  4. 监控预加载对性能的实际影响
  5. 不要过度预加载,可能会影响其他资源加载

Question:会阻塞页面吗
Answer:rel="preload" 本身不会阻塞页面渲染,但需要区分几种情况:
  1. 单纯的 preload
    1
    <link rel="preload" href="style.css" as="style">
  • 不会阻塞 HTML 解析
  • 不会阻塞页面渲染
  • 只是提前下载资源
  1. preload + stylesheet
    1
    2
    <link rel="preload" href="style.css" as="style">
    <link rel="stylesheet" href="style.css">
  • preload 不阻塞
  • 但 stylesheet 仍然会阻塞渲染
  • CSS 仍然是渲染阻塞资源
  1. 使用 onload 延迟应用 CSS
    1
    2
    3
    4
    5
    <!-- 不阻塞的方式 -->
    <link rel="preload" href="style.css" as="style" onload="this.rel='stylesheet'">
    <noscript>
    <link rel="stylesheet" href="style.css">
    </noscript>
  • preload 不阻塞
  • CSS 在加载完成后才应用
  • 可能造成闪烁(FOUC)
  1. 关键 CSS 内联 + 非关键 CSS 预加载
    1
    2
    3
    4
    5
    6
    7
    <style>
    /* 关键 CSS 直接内联 */
    .critical { color: red; }
    </style>

    <!-- 非关键 CSS 预加载 -->
    <link rel="preload" href="non-critical.css" as="style" onload="this.rel='stylesheet'">
  • 关键样式立即生效
  • 非关键样式异步加载
  • 最佳性能实践
最佳实践建议:
  1. 区分关键和非关键资源
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <!-- 关键 CSS:正常加载 -->
    <link rel="stylesheet" href="critical.css">

    <!-- 非关键 CSS:预加载 + 异步应用 -->
    <link
    rel="preload"
    href="non-critical.css"
    as="style"
    onload="this.rel='stylesheet'"
    >
  2. 使用媒体查询优化加载
    1
    2
    3
    4
    5
    6
    7
    8
    <!-- 桌面样式延迟加载 -->
    <link
    rel="preload"
    href="desktop.css"
    as="style"
    media="(min-width: 768px)"
    onload="this.rel='stylesheet'"
    >
  3. 配合 loadCSS 工具使用
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    <script>
    // loadCSS polyfill
    function loadCSS(href) {
    const preload = document.createElement('link');
    preload.rel = 'preload';
    preload.as = 'style';
    preload.href = href;
    preload.onload = function() {
    this.rel = 'stylesheet';
    };
    document.head.appendChild(preload);
    }

    // 使用
    loadCSS('style.css');
    </script>
总结:
  • preload 本身不阻塞
  • 但最终 CSS 应用时仍可能阻塞
  • 需要根据场景选择合适的加载策略
  • 关键 CSS 建议直接加载
  • 非关键 CSS 可以预加载+异步应用

Question:cacheGroups 中的 iconfont.js 设置为 lowest
Answer:在 webpack 配置中,我们可以通过 optimization.splitChunks.cacheGroups 来设置 iconfont.js 的加载优先级。这里有几种方法:
  1. webpack 配置方式
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // webpack.config.js
    module.exports = {
    optimization: {
    splitChunks: {
    cacheGroups: {
    iconfont: {
    test: /iconfont\.js$/,
    name: 'iconfont',
    chunks: 'all',
    priority: -20, // 设置较低优先级
    enforce: true
    }
    }
    }
    }
    }
  2. 在 HTML 中使用 fetchpriority
    1
    2
    <!-- webpack 打包后,在 HTML 模板中设置 -->
    <script src="/iconfont.chunk.js" fetchpriority="low"></script>
  3. 结合 HTML Webpack Plugin
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    // webpack.config.js
    const HtmlWebpackPlugin = require('html-webpack-plugin');

    module.exports = {
    plugins: [
    new HtmlWebpackPlugin({
    template: 'index.html',
    scriptLoading: 'defer', // 使用 defer
    inject: true,
    minify: {
    removeComments: true,
    collapseWhitespace: true
    }
    })
    ],
    optimization: {
    splitChunks: {
    cacheGroups: {
    iconfont: {
    test: /iconfont\.js$/,
    name: 'iconfont',
    chunks: 'all',
    priority: -20,
    enforce: true,
    attributes: {
    fetchpriority: 'low' // 设置加载优先级
    }
    }
    }
    }
    }
    }
  4. 完整的优化配置示例
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    // webpack.config.js
    module.exports = {
    optimization: {
    splitChunks: {
    chunks: 'all',
    minSize: 20000,
    minRemainingSize: 0,
    minChunks: 1,
    maxAsyncRequests: 30,
    maxInitialRequests: 30,
    enforceSizeThreshold: 50000,
    cacheGroups: {
    // 第三方库
    vendors: {
    test: /[\\/]node_modules[\\/]/,
    priority: -10,
    reuseExistingChunk: true
    },
    // iconfont 单独打包
    iconfont: {
    test: /iconfont\.js$/,
    name: 'iconfont',
    chunks: 'all',
    priority: -20,
    enforce: true,
    reuseExistingChunk: true
    },
    // 默认配置
    default: {
    minChunks: 2,
    priority: -30,
    reuseExistingChunk: true
    }
    }
    }
    }
    }
  5. 结合动态导入使用
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // 在代码中动态导入
    const loadIconfont = () => {
    return import(
    /* webpackChunkName: "iconfont" */
    /* webpackPreload: false */
    './iconfont.js'
    );
    };

    // 在适当时机加载
    window.addEventListener('load', () => {
    loadIconfont();
    });
注意事项:
  1. priority 值越小,优先级越低
  2. 确保 iconfont.js 的正则匹配准确
  3. 考虑与其他缓存组的优先级关系
  4. 可以结合 preload/prefetch 策略
  5. 注意浏览器兼容性
建议:
  1. 如果 iconfont 不是首屏必需,可以设置为低优先级
  2. 考虑使用 CSS 的 font-display 属性优化字体加载
  3. 监控加载性能,确保优化效果
  4. 结合实际项目需求调整配置

Question:chunks: 'all' 的意思是什么
Answer:chunks: 'all' 表示对所有类型的 chunks 都进行分割处理,包括:
  1. 三种可选值
    1
    2
    3
    4
    5
    6
    7
    8
    9
    cacheGroups: {
    vendors: {
    chunks: 'initial' // 只处理入口 chunk
    // 或
    chunks: 'async' // 只处理异步 chunk(按需加载的代码)
    // 或
    chunks: 'all' // 处理所有 chunk
    }
    }
  2. 具体含义
  • initial: 只处理入口文件引入的模块
    1
    2
    // 入口文件
    import jQuery from 'jquery'; // 会被处理
  • async: 只处理动态导入的模块
    1
    2
    // 动态导入
    import('./module.js').then(...); // 会被处理
  • all: 处理所有模块
    1
    2
    3
    4
    5
    // 入口文件静态导入
    import jQuery from 'jquery'; // 会被处理

    // 动态导入
    import('./module.js').then(...); // 也会被处理
  1. 实际配置示例
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    module.exports = {
    optimization: {
    splitChunks: {
    cacheGroups: {
    // 处理所有类型的依赖
    vendors: {
    test: /[\\/]node_modules[\\/]/,
    chunks: 'all',
    priority: -10
    },

    // 只处理异步加载的组件
    asyncComponents: {
    test: /[\\/]components[\\/]/,
    chunks: 'async',
    priority: -20
    },

    // 只处理入口依赖的样式
    styles: {
    test: /\.css$/,
    chunks: 'initial',
    priority: -30
    }
    }
    }
    }
    }
  2. 使用建议
  • 通常建议使用 chunks: 'all' 以获得最佳的代码分割效果
  • 特定场景可能需要其他选项:
    • 只想分割异步加载的代码时使用 async
    • 只想分割入口依赖时使用 initial
  • 配合其他选项使用会更有效:
    • minSize(最小尺寸)
    • minChunks(最小引用次数)
    • maxAsyncRequests(最大异步请求数)
    • maxInitialRequests(最大初始请求数)

Question:vue 中页面开始加载是在 js 哪个事件之后进行的
Answer:在 Vue 中,页面加载涉及几个关键的生命周期事件,主要顺序如下:
  1. 创建阶段
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    new Vue({
    beforeCreate() {
    // 1. 实例初始化之后,数据观测和事件配置之前
    // 此时 data、methods 等还不可用
    },
    created() {
    // 2. 实例创建完成后
    // 可以访问 data、computed、methods 等
    // 但还没有挂载 DOM,$el 不可用
    },
    beforeMount() {
    // 3. 挂载开始之前被调用
    // 相关的 render 函数首次被调用
    },
    mounted() {
    // 4. 挂载完成,DOM 已经渲染
    // 可以访问 DOM 元素
    // 这里开始真正的页面加载
    }
    })
  2. 实际使用示例
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    export default {
    created() {
    // 适合做数据初始化
    this.fetchInitialData();
    },

    mounted() {
    // DOM 已经可用,开始加载页面内容
    this.initPage();
    this.loadComponents();
    },

    methods: {
    fetchInitialData() {
    // API 调用等
    },

    initPage() {
    // DOM 操作
    // 第三方库初始化
    }
    }
    }
  3. 异步组件加载
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    export default {
    components: {
    // 异步组件
    AsyncComponent: () => import('./AsyncComponent.vue')
    },

    mounted() {
    // 主页面已加载,但异步组件可能还在加载中
    }
    }
  4. 路由组件加载
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // router.js
    import Vue from 'vue';
    import Router from 'vue-router';

    export default new Router({
    routes: [
    {
    path: '/',
    component: () => import('./views/Home.vue'),
    beforeEnter: (to, from, next) => {
    // 路由级别的加载控制
    next();
    }
    }
    ]
    });
  5. 完整的加载流程控制
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    export default {
    data() {
    return {
    isLoading: true,
    loadError: null
    }
    },

    async created() {
    try {
    // 数据预加载
    await this.fetchInitialData();
    } catch (error) {
    this.loadError = error;
    }
    },

    async mounted() {
    try {
    // DOM 相关初始化
    await this.initComponents();
    this.isLoading = false;
    } catch (error) {
    this.loadError = error;
    }
    },

    methods: {
    async fetchInitialData() {
    // API 调用
    },

    async initComponents() {
    // 组件初始化
    }
    }
    }
  6. 性能优化建议
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    export default {
    created() {
    // 关键数据优先加载
    this.loadCriticalData();

    // 非关键数据延迟加载
    this.$nextTick(() => {
    this.loadNonCriticalData();
    });
    },

    mounted() {
    // DOM 操作放在 mounted 中
    this.initUI();

    // 延迟加载非关键组件
    setTimeout(() => {
    this.loadDeferredComponents();
    }, 0);
    }
    }
总结:
  1. 页面实际加载从 mounted 开始
  2. created 适合做数据准备
  3. mounted 适合做 DOM 操作
  4. 可以利用异步和延迟加载优化性能
  5. 考虑使用加载状态管理用户体验

Question:是在页面加载完成之后,即 window.onload 结束之后吗
Answer:不是的,Vue 的生命周期和 window.onload 是独立的事件。Vue 的 mounted 钩子通常会在 window.onload 之前触发。让我解释一下它们的关系:
  1. 执行顺序
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // Vue 实例
    new Vue({
    mounted() {
    console.log('Vue mounted'); // 1. 先执行
    }
    });

    // window 事件
    window.addEventListener('DOMContentLoaded', () => {
    console.log('DOMContentLoaded'); // 2. 然后是这个
    });

    window.addEventListener('load', () => {
    console.log('Window loaded'); // 3. 最后是这个
    });
  2. 区别
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    export default {
    mounted() {
    // Vue mounted: DOM 已渲染,但外部资源可能还没加载完
    console.log('DOM 已渲染');
    console.log('图片可能还在加载中');
    }
    }

    window.onload = () => {
    // window.onload: 所有资源都加载完成
    console.log('所有资源加载完成');
    console.log('包括图片、样式、脚本等');
    };
  3. 实际应用示例
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    export default {
    mounted() {
    // Vue 已挂载,可以操作 DOM
    this.initUI();

    // 但如果需要等待所有资源
    window.addEventListener('load', () => {
    this.initAfterFullLoad();
    });
    },

    methods: {
    initUI() {
    // 可以进行 DOM 操作
    // 但外部资源可能还没准备好
    },

    initAfterFullLoad() {
    // 所有资源都已加载完成
    // 可以安全地使用依赖外部资源的功能
    }
    }
    }
  4. 最佳实践
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    export default {
    data() {
    return {
    isFullyLoaded: false
    }
    },

    mounted() {
    // 立即需要的初始化
    this.initCriticalFeatures();

    // 监听完整加载
    window.addEventListener('load', () => {
    this.isFullyLoaded = true;
    this.initNonCriticalFeatures();
    });

    // 监听图片加载
    const images = document.querySelectorAll('img');
    images.forEach(img => {
    if (img.complete) {
    this.handleImageLoad(img);
    } else {
    img.addEventListener('load', () => {
    this.handleImageLoad(img);
    });
    }
    });
    },

    methods: {
    initCriticalFeatures() {
    // 关键功能初始化
    // 不依赖外部资源的操作
    },

    initNonCriticalFeatures() {
    // 非关键功能初始化
    // 依赖完整加载的操作
    },

    handleImageLoad(img) {
    // 处理单个图片加载完成
    }
    }
    }
  5. 性能优化建议
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    export default {
    mounted() {
    // 分阶段初始化
    this.initPhase1();

    // 使用 requestAnimationFrame 延迟非关键初始化
    requestAnimationFrame(() => {
    this.initPhase2();
    });

    // 使用 load 事件处理最终初始化
    window.addEventListener('load', () => {
    this.initPhase3();
    });
    },

    methods: {
    initPhase1() {
    // 立即需要的功能
    },

    initPhase2() {
    // 可以稍微延迟的功能
    },

    initPhase3() {
    // 所有资源加载后的功能
    }
    }
    }
总结:
  1. Vue mounted 在 window.onload 之前触发
  2. mounted 时 DOM 已渲染,但外部资源可能未加载完
  3. window.onload 表示所有资源都加载完成
  4. 根据需求选择合适的初始化时机
  5. 可以组合使用不同事件实现最佳性能

Question:是在 DCL 之后 vue 开始渲染吗
Answer:是的,Vue 的渲染是在 DOMContentLoaded (DCL) 事件之后开始的。让我详细解释一下这个过程:
  1. 典型的加载顺序
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    document.addEventListener('DOMContentLoaded', () => {
    console.log('1. DCL'); // 首先触发
    });

    new Vue({
    beforeCreate() {
    console.log('2. Vue beforeCreate');
    },
    created() {
    console.log('3. Vue created');
    },
    beforeMount() {
    console.log('4. Vue beforeMount');
    },
    mounted() {
    console.log('5. Vue mounted');
    }
    }).$mount('#app');

    window.addEventListener('load', () => {
    console.log('6. Window load'); // 最后触发
    });
  2. 实际执行流程
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    <!-- index.html -->
    <html>
    <head>
    <script src="vue.js"></script>
    </head>
    <body>
    <div id="app">
    <!-- Vue 模板 -->
    </div>

    <script>
    // 1. 解析 HTML
    // 2. 触发 DOMContentLoaded
    // 3. Vue 开始初始化和渲染
    // 4. 外部资源继续加载
    // 5. 触发 window.load
    </script>
    </body>
    </html>
  3. 生命周期与事件的关系
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // 监听各个阶段
    document.addEventListener('readystatechange', () => {
    console.log(document.readyState);
    // loading -> interactive(DCL) -> complete(load)
    });

    document.addEventListener('DOMContentLoaded', () => {
    console.log('DOM 准备就绪');
    // Vue 将在这之后开始工作
    });

    new Vue({
    mounted() {
    console.log('Vue 渲染完成');
    // DOM 已经更新
    }
    });
  4. 实际应用示例
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    // 完整的初始化流程
    document.addEventListener('DOMContentLoaded', () => {
    // DOM 已准备好,可以初始化 Vue
    initVueApp();
    });

    function initVueApp() {
    new Vue({
    data: {
    isReady: false
    },

    created() {
    // 数据初始化
    this.initData();
    },

    mounted() {
    // DOM 操作
    this.isReady = true;
    }
    }).$mount('#app');
    }
  5. 性能优化建议
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // 分阶段加载
    document.addEventListener('DOMContentLoaded', () => {
    // 1. 核心功能初始化
    initCoreFeatures();

    // 2. Vue 初始化
    const app = new Vue({
    // ...配置
    });

    // 3. 延迟加载非关键功能
    requestIdleCallback(() => {
    initNonCriticalFeatures();
    });
    });

    // 4. 完整加载后的操作
    window.addEventListener('load', () => {
    initAfterFullLoad();
    });
  6. 常见的加载优化模式
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 1. 关键资源预加载
    <link rel="preload" href="vue.js" as="script">

    // 2. 非关键组件异步加载
    components: {
    'heavy-component': () => import('./HeavyComponent.vue')
    }

    // 3. 条件渲染等待 DOM 准备
    <div v-if="isDomReady">
    <!-- 需要 DOM 准备好的内容 -->
    </div>
总结:
  1. DCL 事件触发表示 DOM 准备就绪
  2. Vue 在 DCL 之后开始初始化和渲染
  3. Vue 的生命周期钩子在 DCL 和 load 之间执行
  4. 可以利用这个顺序来优化加载性能
  5. 考虑使用分阶段加载策略
注意事项:
  • DCL 只保证 DOM 准备好,不包括外部资源
  • Vue 渲染可能在外部资源加载完成之前
  • 需要考虑资源依赖关系进行优化

Question:如果body底部有 script 加载,那么DCL 触发是在 script 加载之前还是之后
Answer:对于位于 body 底部的 script,分几种情况:
  1. 普通 script
    1
    2
    3
    4
    5
    6
    7
    8
    <body>
    <div id="app"></div>

    <!-- 普通脚本 -->
    <script src="large-script.js"></script>

    <!-- DCL 会等这个脚本下载和执行完才触发 -->
    </body>
  2. async script
    1
    2
    3
    4
    5
    6
    7
    8
    <body>
    <div id="app"></div>

    <!-- async 脚本不阻塞 DCL -->
    <script async src="analytics.js"></script>

    <!-- DCL 不会等待 async 脚本 -->
    </body>
  3. defer script
    1
    2
    3
    4
    5
    6
    7
    8
    <body>
    <div id="app"></div>

    <!-- defer 脚本不阻塞 DCL -->
    <script defer src="non-critical.js"></script>

    <!-- DCL 不会等待 defer 脚本 -->
    </body>
  4. 混合情况
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    <body>
    <div id="app"></div>

    <!-- 会阻塞 DCL -->
    <script src="critical.js"></script>

    <!-- 不会阻塞 DCL -->
    <script defer src="non-critical.js"></script>
    <script async src="analytics.js"></script>

    <script>
    // DCL 触发顺序示意
    document.addEventListener('DOMContentLoaded', () => {
    console.log('DCL'); // 在 critical.js 之后,其他可能在之前或之后
    });
    </script>
    </body>
总结:
  1. 普通 script:DCL 在脚本加载和执行之后触发
  2. async script:DCL 不等待这类脚本
  3. defer script:DCL 不等待这类脚本
  4. 内联脚本:DCL 等待执行完成
最佳实践:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<body>
<!-- 页面内容 -->
<div id="app"></div>

<!-- 关键脚本:阻塞 DCL -->
<script src="vue.js"></script>

<!-- 非关键脚本:不阻塞 DCL -->
<script defer src="analytics.js"></script>
<script defer src="non-critical.js"></script>

<!-- 初始化代码 -->
<script>
// 这里的代码会在 vue.js 加载完后执行
// 且 DCL 会等待这段代码执行完
new Vue({
// ...
});
</script>
</body>

Question:@index.html 页面中的这种情况,DCL 是在script加载之前触发吗
Answer:让我分析一下这个 index.html 中的脚本加载情况:
  1. head 中的脚本(会阻塞 DCL):
    1
    2
    3
    4
    5
    6
    7
    8
    <!-- CDN js -->
    <script src="<%= htmlWebpackPlugin.options.cdn.js[i].href %>"></script>

    <!-- jQuery -->
    <script src="//zxxkstatic.zxxk.com/xkwjs/lib/jquery-1.10.2.js?v=0.0128"></script>

    <!-- 阿里播放器 -->
    <script src="https://g.alicdn.com/de/prismplayer/2.8.2/aliplayer-min.js"></script>
  2. body 底部的脚本(都使用了 async,不会阻塞 DCL):
    1
    2
    3
    4
    <script async src="https://downloadnew.zxxk.com/risk-control-dialog/control-iframe-dialog.min.js"></script>
    <script async src="//zxxkstatic.zxxk.com/ued/common/xep/xep.js?v=1.0.0"></script>
    <script async src="//zxxkstatic.zxxk.com/xkwjs/xkw.element-monitor.js"></script>
    <script async src="//zxxkstatic.zxxk.com/Common/Scripts/searchlog/jquery-search-log.js"></script>
在这种情况下:
  1. DCL 会等待 head 中的普通脚本加载和执行完成
  2. 但不会等待 body 底部的 async 脚本
  3. DCL 触发顺序:
    • head 中的脚本加载完成
    • DOM 解析完成
    • DCL 触发
    • async 脚本在某个时间点加载完成并执行
所以对于这个页面:
  • DCL 会在 body 底部那些 async 脚本加载之前触发
  • 但会等待 head 中的普通脚本加载完成
建议优化:
1
2
3
4
5
<!-- head 中非关键脚本也可以使用 defer -->
<script defer src="//zxxkstatic.zxxk.com/xkwjs/lib/jquery-1.10.2.js"></script>

<!-- 或者使用 preload 提前加载 -->
<link rel="preload" href="//zxxkstatic.zxxk.com/xkwjs/lib/jquery-1.10.2.js" as="script">
这样可以让 DCL 更早触发,提升页面性能。
Question:这个项目是一个 vue 项目,如果vue组件中存在使用 jquery 的情况,还能加 defer 吗
Answer:
将 jQuery 加上 defer 在 Vue 项目中可能会有问题,特别是当组件直接依赖 jQuery 时。分析这种情况的依赖关系:
  1. 当前加载顺序:
    1
    jQuery加载 -> HTML解析 -> DCL触发 -> Vue初始化 -> 组件渲染
  2. 如果给 jQuery 加 defer:
    1
    HTML解析 -> DCL触发 -> Vue初始化/jQuery加载(并行) -> 组件渲染
可能出现的问题:
  • jQuery 可能在 Vue 组件初始化时还未加载完成
  • 组件中的 $ 会是 undefined
  • 导致 $ is not defined 错误
几种可能的解决方案:
  1. 不使用 defer,但考虑 preload:
    1
    2
    3
    <!-- 预加载但不延迟执行 -->
    <link rel="preload" href="//zxxkstatic.zxxk.com/xkwjs/lib/jquery-1.10.2.js" as="script">
    <script src="//zxxkstatic.zxxk.com/xkwjs/lib/jquery-1.10.2.js"></script>
  2. 使用 Webpack 的 externals 并改变引入方式:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // webpack.config.js
    module.exports = {
    externals: {
    jquery: 'jQuery'
    }
    }

    // 组件中
    import $ from 'jquery';
    // 或
    const $ = window.jQuery;
  3. 条件检查 jQuery 可用性:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    export default {
    mounted() {
    // 确保 jQuery 已加载
    const checkJQuery = () => {
    if (window.jQuery) {
    this.initWithJQuery();
    } else {
    setTimeout(checkJQuery, 50);
    }
    };
    checkJQuery();
    },
    methods: {
    initWithJQuery() {
    // 使用 jQuery 的代码
    }
    }
    }
结论:
  • 如果组件直接依赖 jQuery,不建议给 jQuery 添加 defer
  • 可以通过其他方式优化(如 preload)
  • 或者重构代码减少对 jQuery 的依赖
  • 最好的做法是在 Vue 项目中减少对 jQuery 的直接使用

Question:可以将外部引用的 css 文件放到底部吗
Answer:
将 CSS 放到底部不是推荐的做法,这可能会导致严重的用户体验问题。原因如下:
  1. 会导致无样式闪烁(FOUC):
    • 页面会先以无样式方式渲染
    • 然后突然应用样式
    • 造成视觉跳动和闪烁
  2. 导致布局偏移(CLS):
    • 内容先以默认样式布局
    • CSS 加载后重新布局
    • 这是 Google Core Web Vitals 中的负面指标
更好的替代方案:
  1. 关键CSS内联 + 非关键CSS异步加载:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <head>
    <!-- 关键CSS内联 -->
    <style>
    /* 关键首屏样式 */
    body { font-family: sans-serif; margin: 0; }
    header { /* 样式... */ }
    </style>

    <!-- 非关键CSS异步加载 -->
    <link rel="preload" href="styles.css" as="style" onload="this.rel='stylesheet'">
    <noscript><link rel="stylesheet" href="styles.css"></noscript>
    </head>
  2. 使用 media 属性延迟非关键样式:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <head>
    <!-- 关键样式立即加载 -->
    <link rel="stylesheet" href="critical.css">

    <!-- 印刷样式延迟加载 -->
    <link rel="stylesheet" href="print.css" media="print">

    <!-- 桌面样式延迟加载(手机上) -->
    <link rel="stylesheet" href="desktop.css" media="(min-width: 1024px)">
    </head>
  3. 使用工具提取关键CSS:
    1
    2
    // 使用工具如 Critical, CriticalCSS, Penthouse 等
    // 自动提取首屏关键CSS
如果一定要将部分 CSS 放在底部(不推荐),可以使用 JavaScript 动态加载:
1
2
3
4
5
6
7
8
9
<script>
// 在 DOMContentLoaded 后加载非关键CSS
document.addEventListener('DOMContentLoaded', function() {
var link = document.createElement('link');
link.rel = 'stylesheet';
link.href = 'non-critical.css';
document.body.appendChild(link);
});
</script>
最佳实践总结:
  1. 关键 CSS 放在 <head>
  2. 使用预加载、延迟加载等技术优化非关键 CSS
  3. 避免将所有 CSS 放到底部
  4. 考虑使用现代工具提取和优化 CSS

Question:使用 preload 进行异步加载,异步体现在哪个地方
Answer:
preload 的异步特性主要体现在几个方面:
  1. 并行下载不阻塞渲染
    1
    <link rel="preload" href="style.css" as="style" onload="this.rel='stylesheet'">
  • 浏览器会在解析到这行代码时立即开始下载资源
  • 但不会阻塞 HTML 解析和页面渲染
  • 页面可以继续加载,同时 CSS 在后台下载
  1. 应用与下载分离
    1
    2
    3
    4
    5
    6
    7
    <!-- 下载过程 -->
    <link rel="preload" href="style.css" as="style" onload="this.rel='stylesheet'">

    <!-- 或者 -->
    <link rel="preload" href="script.js" as="script">
    <!-- 应用过程 -->
    <script src="script.js" defer></script>
  • 资源的下载和应用是分开的两个步骤
  • 资源可以在不被使用的情况下提前下载
  1. 不阻塞 DOMContentLoaded 事件
    1
    2
    3
    4
    5
    6
    <link rel="preload" href="large-file.css" as="style" onload="this.rel='stylesheet'">
    <script>
    document.addEventListener('DOMContentLoaded', () => {
    console.log('DCL fired!'); // 不会等待 preload 完成
    });
    </script>
  • 即使大型 CSS 文件还在下载中,DCL 事件也会触发
  • 允许基于 DCL 的脚本更早执行
  1. 手动控制应用时机
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <link rel="preload" href="style.css" as="style">
    <script>
    // 通过 JS 控制应用时机
    window.addEventListener('load', () => {
    const link = document.createElement('link');
    link.rel = 'stylesheet';
    link.href = 'style.css'; // 已经被预加载
    document.head.appendChild(link);
    });
    </script>
  • 开发者可以精确控制资源的应用时机
  • 资源已经在缓存中,应用时不需要再下载
  1. 示例:异步应用 CSS
    1
    2
    3
    4
    5
    6
    7
    8
    9
    <!-- 关键CSS内联 -->
    <style>
    /* 最基本的样式 */
    body { margin: 0; font-family: sans-serif; }
    </style>

    <!-- 异步加载其他CSS -->
    <link rel="preload" href="full-styles.css" as="style" onload="this.rel='stylesheet'">
    <noscript><link rel="stylesheet" href="full-styles.css"></noscript>
  • 页面会首先使用内联的基本样式渲染
  • 完整样式异步加载,完成后自动应用
  • 用户可以看到基本内容,而不必等待所有样式
总结:preload 的异步性主要体现在下载过程与渲染流程的解耦,以及资源获取与应用的分离,这使得页面加载更高效,用户体验更流畅;